﻿using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;

namespace Moonlite.Modules.Editors.ImageViewer.Tga
{
    /// <summary>
    /// Represents a class that can read and write images that have been compressed using the Truevision Graphics Adapter image compression method.
    /// </summary>
    public unsafe static class TgaImageLoader
    {
        [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
        private static extern IntPtr MemCopy(IntPtr dest, IntPtr src, UIntPtr count);

        /// <summary>
        /// Reads a <see cref="TgaHeader"/> data structure from the specified <see cref="System.IO.BinaryReader"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.BinaryReader"/>.</param>
        /// <returns>The read <see cref="TgaHeader"/>.</returns>
        private static TgaHeader ReadHeader(BinaryReader stream)
        {
            TgaHeader header = new TgaHeader();
            header.idLength = stream.ReadByte();
            header.colorMapType = stream.ReadByte();
            header.imageType = stream.ReadByte();
            header.colorMapIndex = stream.ReadUInt16();
            header.colorMapLength = stream.ReadUInt16();
            header.colorMapSize = stream.ReadByte();
            header.xOrigin = stream.ReadUInt16();
            header.yOrigin = stream.ReadUInt16();
            header.width = stream.ReadUInt16();
            header.height = stream.ReadUInt16();
            header.pixelSize = stream.ReadByte();
            header.flags = stream.ReadByte();
            return header;
        }

        /// <summary>
        /// Determines whether the specified <see cref="TgaHeader"/> is supported.
        /// </summary>
        /// <param name="header">The <see cref="TgaHeader"/>.</param>
        /// <returns>
        /// 	<c>true</c> if the specified <see cref="TgaHeader"/> is supported; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsSupported(TgaHeader header)
        {
            if (header.imageType != (byte)TgaType.TGA_TYPE_INDEXED &&
                header.imageType != (byte)TgaType.TGA_TYPE_RGB &&
                header.imageType != (byte)TgaType.TGA_TYPE_GREY &&
                header.imageType != (byte)TgaType.TGA_TYPE_RLE_INDEXED &&
                header.imageType != (byte)TgaType.TGA_TYPE_RLE_RGB &&
                header.imageType != (byte)TgaType.TGA_TYPE_RLE_GREY)
            {
                return false;
            }

            if (header.imageType == (byte)TgaType.TGA_TYPE_INDEXED ||
                header.imageType == (byte)TgaType.TGA_TYPE_RLE_INDEXED)
            {
                if (header.colorMapLength > 256 || header.colorMapSize != 24)
                {
                    return false;
                }
            }

            if (header.width == 0 || header.height == 0)
            {
                return false;
            }

            if (header.pixelSize != 8 && header.pixelSize != 16 &&
                header.pixelSize != 24 && header.pixelSize != 32)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Converts the specified <see cref="System.Byte"/> array to a <see cref="System.Byte"/> pointer.
        /// </summary>
        /// <param name="input">The input <see cref="System.Byte"/> array.</param>
        /// <returns>The converted <see cref="System.Byte"/> array;</returns>
        private static byte* ConvertByteArrayToBytePointer(byte[] input)
        {
            fixed (byte* bytePtr = input)
            {
                return bytePtr;
            }
        }

        /// <summary>
        /// Reads Truevision Graphics Adapter image data from the specified <see cref="System.IO.BinaryReader"/> to the specified <see cref="System.Drawing.Bitmap"/>.
        /// </summary>
        /// <param name="reader">The <see cref="System.IO.BinaryReader"/>.</param>
        /// <param name="header">The <see cref="TgaHeader"/>.</param>
        /// <param name="image">The <see cref="System.Drawing.Bitmap"/>.</param>
        /// <returns>Whether the reading was succesful.</returns>
        private static bool ReadData(BinaryReader reader, TgaHeader header, out Bitmap image)
        {
            image = new Bitmap(header.width, header.height, PixelFormat.Format32bppRgb);
            TgaHeaderInfo headerInfo = new TgaHeaderInfo(header);
            if (!headerInfo.supported)
            {
                Console.WriteLine("This Tga file is not supported.");
                return false;
            }

            int alphaBits = header.flags & 0xf;
            if ((header.pixelSize == 32) && (header.flags & 0xf) != 0)
            {
                image = new Bitmap(header.width, header.height, PixelFormat.Format32bppArgb);
            }

            uint pixelSize = (uint)(header.pixelSize / 8);
            uint size = (uint)(header.width * header.height * pixelSize);
            if (size < 1)
            {
                Console.WriteLine("This Tga file is broken with size " + size + ".");
                return false;
            }

            byte[] palette = new byte[768];
            if (headerInfo.pal)
            {
                palette = reader.ReadBytes(header.colorMapLength * 3);
            }

            byte[] imageBytes = new byte[size];
            if (headerInfo.rle)
            {
                byte* dst = ConvertByteArrayToBytePointer(imageBytes);
                int num = (int)size;
                while (num > 0)
                {
                    byte c = reader.ReadByte();
                    uint count = (uint)((c & 0x7f) + 1);
                    num -= (int)(count * pixelSize);
                    if ((c & 0x80) != 0)
                    {
                        Debug.Assert(pixelSize <= 8);
                        byte[] pixel = new byte[8];
                        pixel = reader.ReadBytes((int)pixelSize);
                        do
                        {
                            MemCopy((IntPtr)dst, (IntPtr)ConvertByteArrayToBytePointer(pixel), (UIntPtr)pixelSize);
                            dst += pixelSize;
                        }
                        while (--count != 0);
                    }
                    else
                    {
                        count *= pixelSize;
                        dst = ConvertByteArrayToBytePointer(reader.ReadBytes((int)count));
                        dst += count;
                    }
                }
            }
            else
            {
                imageBytes = reader.ReadBytes((int)size);
            }

            int yStart;
            int yStep;
            int yEnd;
            if ((header.flags & TgaConstants.TGA_ORIGIN_UPPER) != 0)
            {
                yStart = 0;
                yStep = 1;
                yEnd = header.height;
            }
            else
            {
                yStart = header.height - 1;
                yStep = -1;
                yEnd = 1;
            }

            byte* src = ConvertByteArrayToBytePointer(imageBytes);
            for (int y = yStart; y != yEnd; y += yStep)
            {
                if (headerInfo.pal)
                {
                    for (int x = 0; x < header.width; x++)
                    {
                        byte index = *src++;
                        image.SetPixel(x, y, Color.FromArgb((int)palette[3 * index + 2], (int)palette[3 * index + 1], (int)palette[3 * index + 0]));
                    }
                }
                else if (headerInfo.grey)
                {
                    for (int x = 0; x < header.width; x++)
                    {
                        image.SetPixel(x, y, Color.FromArgb((int)(*src), (int)(*src), (int)(*src)));
                        src++;
                    }
                }
                else
                {
                    if (header.pixelSize == 16)
                    {
                        Console.WriteLine("pixelSize of 16 is not supported.");
                        return false;
                    }
                    else if (header.pixelSize == 24)
                    {
                        for (int x = 0; x < header.width; x++)
                        {
                            image.SetPixel(x, y, Color.FromArgb(src[2], src[1], src[0]));
                            src += 3;
                        }
                    }
                    else if (header.pixelSize == 32)
                    {
                        for (int x = 0; x < header.width; x++)
                        {
                            byte alpha = (byte)(src[3] << (8 - alphaBits));
                            image.SetPixel(x, y, Color.FromArgb(src[2], src[1], src[0], alpha));
                            src += 4;
                        }
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Reads the specified <see cref="System.IO.Stream"/> and creates a <see cref="System.Drawing.Bitmap"/> using the Truevision Graphics Adapter image compression algorithm.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.Stream"/>.</param>
        /// <returns>The created <see cref="System.Drawing.Bitmap"/>.</returns>
        public static Bitmap Read(Stream stream)
        {
            bool dummy = true;
            return Read(stream, out dummy);
        }

        /// <summary>
        /// Reads the specified <see cref="System.IO.Stream"/> and creates a <see cref="System.Drawing.Bitmap"/> using the Truevision Graphics Adapter image compression algorithm.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.Stream"/>.</param>
        /// <param name="result">A value indicating whether the reading was succesful.</param>
        /// <returns>The created <see cref="System.Drawing.Bitmap"/>.</returns>
        public static Bitmap Read(Stream stream, out bool result)
        {
            using (BinaryReader reader = new BinaryReader(stream))
            {
                TgaHeader header = ReadHeader(reader);
                Bitmap bitmap = null;
                result = false;
                if (stream.Position == stream.Length - 1)
                {
                    Console.WriteLine("This Tga file is not valid.");
                    return bitmap;
                }

                if (!IsSupported(header))
                {
                    Console.WriteLine("This Tga file is not supported.");
                    return bitmap;
                }

                result = ReadData(reader, header, out bitmap);
                if (result == false)
                {
                    Console.WriteLine("Error loading Tga file.");
                    return bitmap;
                }

                return bitmap;
            }
        }

        /// <summary>
        /// Writes the specified <see cref="System.Drawing.Bitmap"/> to the specified <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.Stream"/>.</param>
        /// <param name="bitmap">The <see cref="System.Drawing.Bitmap"/>.</param>
        /// <returns>A value indicating whether the writing was sucessful.</returns>
        public static bool Write(Stream stream, Bitmap bitmap)
        {
            using (BinaryWriter writer = new BinaryWriter(stream))
            {
                bool hasAlpha = bitmap.PixelFormat == PixelFormat.Format32bppArgb;
                for (int i = 0; i < 12; i++)
                {
                    writer.Write(TgaConstants.TargaMagic[i]);
                }

                writer.Write(bitmap.Width);
                writer.Write(bitmap.Height);
                writer.Write(hasAlpha ? 32 : 24);
                writer.Write(hasAlpha ? 0x24 : 0x20);
                for (int x = 0; x < bitmap.Height; x++)
                {
                    for (int y = 0; y < bitmap.Width; y++)
                    {
                        Color pixel = bitmap.GetPixel(x, y);
                        writer.Write(pixel.B);
                        writer.Write(pixel.G);
                        writer.Write(pixel.R);
                        if (hasAlpha)
                        {
                            writer.Write(pixel.A);
                        }
                    }
                }

                return true;
            }
        }
    }
}
